package	editor;

import editor.headers.Header;
import editor.widgets.*;
import event.*;
import lang.Encoder;
import lang.Language;
import lang.Loader;
import model.Project;
import model.caret.Caret;
import model.caret.HeaderCaret;
import model.caret.TextCaret;
import model.code.*;
import model.code.line.Line;
import model.code.line.LineList;
import model.code.line.SyntaxError;
import model.symbol.Symbol;
import observer.ObservedList;
import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.wc.SVNRevision;
import perf.PerformanceTestInfo;
import util.ClipboardUtil;
import util.FileUtil;
import util.Triple;
import widgets.*;

import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

/**
 * This is the main class of PwnIDE.
 *
 * @author Neil Dickson
 */
public class PwnIDE extends JFrame implements CustomDialog.ModalParent, WindowFocusListener {
	/**
	 * The PwnIDE instance.
	 */
	public static PwnIDE					editor;
	public static UIController				ui;
	public static TreeMap<String,String>	settings;

	public static CustomProgressBar			progress;

	public static int						operatingSystem;
	public static final int		OS_UNKNOWN				= 0;
	public static final int		OS_WINDOWS				= 1;
	public static final int		OS_MAC					= 2;
	public static final int		OS_UNIX					= 3;

	public static final int		VERSION_ERA_NUMBER		= 0;
	public static final int		VERSION_STAGE_NUMBER	= 2;
	public static final String	VERSION_STAGE_NAME		= "Alpha";
	public static final int		VERSION_RELEASE_NUMBER	= 6;
	public static final String	VERSION_BUILD_CHAR		= "";
	public static final	String	VERSION_DATE			= "September 25th, 2011";
	public static final String	VERSION_STRING			= VERSION_ERA_NUMBER+"."+VERSION_STAGE_NUMBER+"."+VERSION_RELEASE_NUMBER+VERSION_BUILD_CHAR;

	public static final String	ABOUT_TEXT				= "Inventor IDE version "+VERSION_STRING+"  ("+VERSION_STAGE_NAME+")\n"+VERSION_DATE+"\n\nDeveloped as part of Code Cortex, where the software enters your brain.\n\nCopyright \u00A9 2007-2009 Neil Dickson\nhttp://www.codecortex.com/ide/\nSource code licensed under GPL v.2.0";
	public static final String	ABOUT_CODE_CORTEX_TEXT	= "The goal of Code Cortex is to make development of large-scale, high-performance software\nfeasible, despite the obstacles currently facing developers.\n\nIt is a long, hard road, but together, we will make it a reality.\n\nhttp://www.codecortex.com/";

	public static final String	CONFIG_FILE				= "Inventor.ini";
	public static 		String	PWNIDE_DIR;
	public static 		String	IMAGE_DIR;

	public static final String	FEEDBACK_EMAIL			= "inventoride"+".feedback"+"@g"+"mai"+"l."+new String("com");

	private CustomToolbar		toolbar;
	private	CustomButton		runButton;
	private	CustomButton		stopButton;

	public final CustomSplitPane	messageSplit;
	public final JPanel				findAndTabs;
	private final CustomTabPane		tabs;
	private final ProjectPanel		projectPanel;

	private MessagePopup		aboutPwnIDEMessage;
	private MessagePopup		aboutCodeCortexMessage;

	private Project				project;

	private	UndoStack			undos;

	public	CustomDialog		modalDialog;

	public	boolean				justHadSuccessfulCompile;
	public	Process				runningProcess;

	private static final FileFilter PROJECT_FILE_FILTER = new CustomFileFilter(new String[]{"pwn"},"Inventor Project Files (*.pwn)");
	private static final FileFilter SUPPORTED_FILES_FILTER = new CustomFileFilter(new String[]{"asm","inc"},"Supported Files (*.asm,*.inc)");

	private PwnIDE(String projectName) {
		super("Inventor IDE");
		editor = this;
		modalDialog = null;
		undos = new UndoStack();
		justHadSuccessfulCompile = false;

		setIconImage(Images.PWNIDE_ICON_IMAGE);

		setLayout(new BorderLayout(0,0));

		settings = loadSettings();
		if (settings==null) {
			settings = new TreeMap<String, String>();
		}

		toolbar = new CustomToolbar();
		CustomButton newProjectButton = new ImageButton(Images.NEW_PROJECT,Images.NEW_PROJECT_HOVER,Images.NEW_PROJECT_DOWN,Images.NEW_PROJECT_DISABLED);
		toolbar.addButton(newProjectButton);
		CustomButton saveButton = new ImageButton(Images.SAVE,Images.SAVE_HOVER,Images.SAVE_DOWN,Images.SAVE_DISABLED);
		toolbar.addButton(saveButton);
		CustomButton addFileButton = new ImageButton(Images.NEW_FILE,Images.NEW_FILE_HOVER,Images.NEW_FILE_DOWN,Images.NEW_FILE_DISABLED);
		toolbar.addButton(addFileButton);
		toolbar.addSeparator();
		CustomButton newFunctionButton = new ImageButton(Images.NEW_FUNCTION,Images.NEW_FUNCTION_HOVER,Images.NEW_FUNCTION_DOWN,Images.NEW_FUNCTION_DISABLED);
		toolbar.addButton(newFunctionButton);
		CustomButton newStructureButton = new ImageButton(Images.NEW_STRUCTURE,Images.NEW_STRUCTURE_HOVER,Images.NEW_STRUCTURE_DOWN,Images.NEW_STRUCTURE_DISABLED);
		toolbar.addButton(newStructureButton);
		CustomButton newVariableButton = new ImageButton(Images.NEW_VARIABLE,Images.NEW_VARIABLE_HOVER,Images.NEW_VARIABLE_DOWN,Images.NEW_VARIABLE_DISABLED);
		toolbar.addButton(newVariableButton);
		CustomButton newConstantsButton = new ImageButton(Images.NEW_CONSTANTS,Images.NEW_CONSTANTS_HOVER,Images.NEW_CONSTANTS_DOWN,Images.NEW_CONSTANTS_DISABLED);
		toolbar.addButton(newConstantsButton);
		CustomButton newSectionButton = new ImageButton(Images.NEW_SECTION,Images.NEW_SECTION_HOVER,Images.NEW_SECTION_DOWN,Images.NEW_SECTION_DISABLED);
		toolbar.addButton(newSectionButton);
		toolbar.addSeparator();
		runButton = new ImageButton(Images.RUN,Images.RUN_HOVER,Images.RUN_DOWN,Images.RUN_DISABLED);
		stopButton = new ImageButton(Images.STOP,Images.STOP_HOVER,Images.STOP_DOWN,Images.STOP_DISABLED);
		toolbar.addButton(runButton);
		toolbar.addSeparator();
		CustomButton findButton = new ImageButton(Images.FIND,Images.FIND_HOVER,Images.FIND_DOWN,Images.FIND_DISABLED);
		toolbar.addButton(findButton);
		CustomButton findRefsButton = new ImageButton(Images.FIND_REFS,Images.FIND_REFS_HOVER,Images.FIND_REFS_DOWN,Images.FIND_REFS_DISABLED);
		toolbar.addButton(findRefsButton);
		CustomButton goToDefButton = new ImageButton(Images.GO_TO_DEF,Images.GO_TO_DEF_HOVER,Images.GO_TO_DEF_DOWN,Images.GO_TO_DEF_DISABLED);
		toolbar.addButton(goToDefButton);
		toolbar.addSeparator();
		CustomButton settingsButton = new ImageButton(Images.SETTINGS,Images.SETTINGS_HOVER,Images.SETTINGS_DOWN,Images.SETTINGS_DISABLED);
		toolbar.addButton(settingsButton);
		toolbar.addSeparator();
		CustomButton infoButton = new ImageButton(Images.INFO,Images.INFO_HOVER,Images.INFO_DOWN,Images.INFO_DISABLED);
		toolbar.addButton(infoButton);
		CustomButton feedbackButton = new ImageButton(Images.FEEDBACK,Images.FEEDBACK_HOVER,Images.FEEDBACK_DOWN,Images.FEEDBACK_DISABLED);
		toolbar.addButton(feedbackButton);
		add(toolbar,BorderLayout.NORTH);

		ActionListener actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				JFileChooser chooser = (project!=null) ? (new JFileChooser(project.getFile().getParentFile())) : (new JFileChooser());
				chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
				chooser.setMultiSelectionEnabled(false);
				chooser.setFileFilter(PROJECT_FILE_FILTER);
				if (chooser.showOpenDialog(PwnIDE.this)==JFileChooser.APPROVE_OPTION) {
					File file = chooser.getSelectedFile();
					if (file.exists()) {
						openProject(file);
					}
					else {
						if (!file.getName().toLowerCase().endsWith(".pwn")) {
							file = new File(file.getPath()+".pwn");
						}
						if (file.exists()) {
							openProject(file);
						}
						else {
							createNewProject(file);
						}
					}
				}
			}
		};
		newProjectButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				saveAll();
			}
		};
		saveButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				ui.createFunction();
			}
		};
		newFunctionButton.addActionListener(actionListener);
		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				ui.createStructure();
			}
		};
		newStructureButton.addActionListener(actionListener);
		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				ui.createVariable();
			}
		};
		newVariableButton.addActionListener(actionListener);
		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				ui.createConstants();
			}
		};
		newConstantsButton.addActionListener(actionListener);
		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				ui.createSection();
			}
		};
		newSectionButton.addActionListener(actionListener);
		actionListener = new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				addFile();
			}
		};
		addFileButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				run();
			}
		};
		runButton.addActionListener(actionListener);
		stopButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				CodeFile file = ui.getCurrentCodeFile();
				if (file!=null) {
					if (file.getCaretRef().get() instanceof TextCaret) {
						TextCaret caret = (TextCaret)file.getCaretRef().get();
						LineList list = caret.getLineList();
						PwnIDE.ui.toFindBar((caret.isEmpty()) ? "" : list.get(caret.getFirstLine()).toString(caret.getFirstLineCol(),(caret.getFirstLine()==caret.getLastLine()) ? caret.getLastLineCol() : Integer.MAX_VALUE));
					}
				}
			}
		};
		findButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				CodeFile file = ui.getCurrentCodeFile();
				if (file!=null) {
					if (file.getCaretRef().get() instanceof TextCaret) {
						TextCaret caret = (TextCaret)file.getCaretRef().get();
						LineList list = caret.getLineList();
						if (list!=null) {
							Line line = list.get(caret.getHeadLine());
							if (line!=null) {
								Symbol symbol;
								if (caret.getTailLine() > caret.getHeadLine() || (caret.getTailLine()==caret.getHeadLine() && caret.getTailCol()>=caret.getHeadCol())) {
									symbol = line.getSymbolAt(caret.getHeadCol());
								}
								else {
									symbol = line.getSymbolAt(caret.getHeadCol()-1);
								}
								if (symbol!=null) {
									ui.findReferences(symbol);
								}
							}
						}
					}
					else if (file.getCaretRef().get() instanceof HeaderCaret) {
						HeaderCaret caret = (HeaderCaret)file.getCaretRef().get();
						ui.findReferences(caret.getLast());
					}
				}
			}
		};
		findRefsButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				CodeFile file = ui.getCurrentCodeFile();
				if (file!=null) {
					if (file.getCaretRef().get() instanceof TextCaret) {
						TextCaret caret = (TextCaret)file.getCaretRef().get();
						LineList list = caret.getLineList();
						if (list!=null) {
							Line line = list.get(caret.getHeadLine());
							if (line!=null) {
								Symbol symbol;
								if (caret.getTailLine() > caret.getHeadLine() || (caret.getTailLine()==caret.getHeadLine() && caret.getTailCol()>=caret.getHeadCol())) {
									symbol = line.getSymbolAt(caret.getHeadCol());
								}
								else {
									symbol = line.getSymbolAt(caret.getHeadCol()-1);
								}
								if (symbol!=null) {
									ui.goToDefinition(symbol);
								}
							}
						}
					}
					else {
						ui.goToCaret(file.getCaretRef().get());
					}
				}
			}
		};
		goToDefButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				showProjectOptions();
//				ErrorHandler.showNotImplementedMessage("Sorry, the settings panel hasn't been made yet.  This one's top priority, because I need it immediately.");
			}
		};
		settingsButton.addActionListener(actionListener);

		aboutPwnIDEMessage = new MessagePopup(editor,Images.PWNIDE_LOGO,ABOUT_TEXT);
		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				aboutPwnIDEMessage.setVisible(true);
			}
		};
		infoButton.addActionListener(actionListener);

		actionListener = new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				ErrorHandler.showNotImplementedMessage("Sorry, the feedback service hasn't been made yet.  This one's a high priority.\nFor now, please visit the forum at www.codecortex.com/forum/\nor email "+FEEDBACK_EMAIL+" with \"Inventor\" in the subject line.");
			}
		};
		feedbackButton.addActionListener(actionListener);

//		optionsMenu = new JMenu("Options");
//		projectOptionsItem = new JMenuItem("Project Options...");
//		actionListener = new ActionListener(){
//			public void actionPerformed(ActionEvent e) {
//				showProjectOptions();
//			}
//		};
//		projectOptionsItem.addActionListener(actionListener);
//		optionsMenu.add(projectOptionsItem);
//		menus.add(optionsMenu);

//		aboutCodeCortexMessage = new MessagePopup(editor,Images.CODE_CORTEX_LOGO,ABOUT_CODE_CORTEX_TEXT);
//		aboutCodeCortexItem = new JMenuItem("About Code Cortex...", Images.CODE_CORTEX_ICON);
//		aboutCodeCortexItem.addActionListener(new ActionListener(){
//			public void actionPerformed(ActionEvent e) {
//				aboutCodeCortexMessage.setVisible(true);
//			}
//		});
//		helpMenu.add(aboutCodeCortexItem);
//		menus.add(helpMenu);


		tabs = new CustomTabPane(){
			public void addTabAt(String name, String toolTip, Component component, ImageIcon icon, int index, boolean autoSelect) {
				if (project!=null) {
					ProjectFile file = PwnIDE.ui.getFileFromComponent(component);
					// Check if the file's already in the list.  This happens when tabs are being opened based on the list.
					int oldIndex = project.getOpenFiles().indexOf(file);
					if (oldIndex>=0 && oldIndex!=index) {
						project.getOpenFiles().remove(oldIndex);
					}
					if (oldIndex<0 || oldIndex!=index) {
						project.getOpenFiles().add(index,file);
//						System.out.println("Adding file at index "+index);
					}
				}
				super.addTabAt(name,toolTip,component,icon,index,autoSelect);
				PwnIDE.ui.updateFocus();
			}
			protected void select(int index) {
				if (project!=null) {
					project.setActiveFile(project.getOpenFiles().get(index));
//					System.out.println("Selecting file at index "+index);
				}
				super.select(index);
				PwnIDE.ui.updateFocus();
			}
			public void removeTabAt(int index) {
				if (project!=null) {
					project.getOpenFiles().remove(index);
//					System.out.println("Removing file at index "+index);
				}
				super.removeTabAt(index);
				PwnIDE.ui.updateFocus();
			}
		};
		if (progress!=null) {
			progress.setStatus("Loading lanugage data...",progress.getCompletion());
		}
		Language.loadLanguages();
		Function.setTestNameObserver(PerformanceTestInfo.nameObserver);
		project = null;
		if (projectName!=null) {
			try {
				project = new Project(new File(projectName),progress);
			}
			catch (IOException e) {
				ErrorHandler.showMessage("Sorry, an error occurred trying to open the project \""+projectName+"\".");
			}
			catch (Exception e) {
				ErrorHandler.showException(e);
			}
		}
		else if (settings!=null) {
//			for (String setting : settings.keySet()) {
//				System.out.println(setting+" = "+settings.get(setting));
//			}
			String projectFile = settings.get("MostRecentProject");
			if (projectFile!=null) {
//				System.out.println("Loading Project: "+projectFile);
				String validFilePath = getValidFilePath(projectFile);
				try {
					project = new Project(new File(validFilePath),progress);
				}
				catch (IOException e) {
					ErrorHandler.showMessage("Sorry, an error occurred trying to open the project \""+validFilePath+"\".");
				}
//				System.out.println("Done Loading Project: "+projectFile);
			}
		}
		else {
			System.err.println("Unable to load configuration");
		}

		// Add the components to the window
		projectPanel = new ProjectPanel(this,project);
		CustomScrollPane scroll = new CustomScrollPane(projectPanel);
		scroll.setAlignmentY(0);
		scroll.setAlignmentX(0);
		findAndTabs = new JPanel(new BorderLayout(0,0)) {
			public void reshape(int x, int y, int w, int h) {
				super.reshape(x, y, w, h);
				doLayout();
			}
		};
		findAndTabs.add(tabs,BorderLayout.CENTER);
		CustomSplitPane projectSplit = new CustomSplitPane(scroll,findAndTabs,160,8);
		messageSplit = new CustomSplitPane(projectSplit,new JPanel(),0,8,true);
		projectPanel.setBackground(Color.WHITE);
		add(messageSplit,BorderLayout.CENTER);

		// Set the size of the window and various other options
		setBounds(0,0,1024,768);
		setExtendedState(getExtendedState() | MAXIMIZED_BOTH);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				saveAll();
				saveSettings(settings);
			}
		});

		// Add the global key event handler
		KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
		focusManager.addKeyEventDispatcher(new KeyEventDispatcher(){
			public boolean dispatchKeyEvent(KeyEvent e) {
				try {
					return handleCommonShortcuts(e);
				}
				catch (Throwable t) {
					ErrorHandler.showException(t);
					return true;
				}
			}
		});

		addWindowFocusListener(this);
		ui = new UIController(this,projectSplit,tabs);
		ui.showOpenFiles();

		// FIXME: The viewport of the scrollpanes will be the invalid size (0,0) and at an incorrect position, allowing invalid scrolling.
		// FIXME: The LineField with the Caret must be made active.
	}

	public void run() {
		if (project!=null) {
			try {
				Process runningProcessCopy = runningProcess;
				if (runningProcessCopy!=null) {
					// This should hopefully work even if the process has just finished
					runningProcessCopy.destroy();
					return;
				}
				ArrayList<SyntaxError> errors = new ArrayList<SyntaxError>();
				System.out.println("Starting Compilation");
				long startTime = System.currentTimeMillis();
				String filename = Encoder.encodeProject(project,errors);
				if (!errors.isEmpty()) {
					showErrors(errors);
				}
				else {
					System.out.println("Compilation Completed in "+(System.currentTimeMillis()-startTime)+"ms with No Errors");
					runCompiledProgram(filename,null);
				}
			}
			catch (Throwable e) {
				ErrorHandler.showException(e);
			}
		}
	}

	public void showErrors(ArrayList<SyntaxError> errors) {
		// Setting justHadSuccessfulCompile should be redundant, but it might as well be done explicitly here
		justHadSuccessfulCompile = false;
		System.out.println("Compilation Complete with "+errors.size()+" errors");
//					for (SyntaxError error : errors) {
//						System.out.println(error);
//					}
		HashMap<String,ArrayList<SyntaxError>> organizedErrors = new HashMap<String,ArrayList<SyntaxError>>();
		for (SyntaxError error : errors) {
			Line line = error.getLine();
			if (line!=null && line.getList()!=null && line.getList().getParent()!=null && line.getList().getParent().getFile()!=null) {
				CodeFile file = line.getList().getParent().getFile();
				String name = file.getName().toString();
				ArrayList<SyntaxError> fileErrors = organizedErrors.get(name);
				if (fileErrors==null) {
					fileErrors = new ArrayList<SyntaxError>();
					organizedErrors.put(name,fileErrors);
				}
				fileErrors.add(error);
			}
			else {
				ArrayList<SyntaxError> nowhereErrors = organizedErrors.get("(not in a particular file)");
				if (nowhereErrors==null) {
					nowhereErrors = new ArrayList<SyntaxError>();
					organizedErrors.put("(not in a particular file)",nowhereErrors);
				}
				nowhereErrors.add(error);
			}
		}
		int numFiles = organizedErrors.keySet().size();
		int numRows = 1+numFiles+errors.size();
		int[] levels = new int[numRows];
		String[] texts = new String[numRows];
		Caret[] carets = new Caret[numRows];
		ImageIcon[] icons = new ImageIcon[numRows];
		levels[0] = 0;
		texts[0] = errors.size()+" errors in "+numFiles+" file"+(numFiles!=1 ? "s" : "");
		carets[0] = null;
		icons[0] = Images.ERROR_ICON;
		int row = 1;
		for (String file : organizedErrors.keySet()) {
			levels[row] = 1;
			texts[row] = file;
			ArrayList<SyntaxError> errorsInFile = organizedErrors.get(file);
			Line line = errorsInFile.get(0).getLine();
			if (line!=null && line.getList()!=null && line.getList().getParent()!=null && line.getList().getParent().getFile()!=null) {
				CodeFile codeFile = line.getList().getParent().getFile();
				carets[row] = new TextCaret(codeFile.getCaretRef(),codeFile.getName(),0,0,0);
				icons[row] = codeFile.getIcon();
			}
			else {
				carets[row] = null;
				icons[row] = null;
			}
			++row;
			for (SyntaxError error : errorsInFile) {
				TextCaret caret = error.getCaret();
				levels[row] = 2;
				texts[row] = (caret!=null) ? caret.getLineList().getParent()+"("+caret.getFirstLine()+","+caret.getFirstLineCol()+"): " : "(no location): ";
				texts[row] += error.getDescription();
				carets[row] = caret;
				icons[row] = Images.ERROR_ICON;
				++row;
			}
		}
		ReferencePanel errorPanel = new ReferencePanel(this,levels,texts,carets,icons);
		CustomScrollPane scroll = new CustomScrollPane(errorPanel);
		errorPanel.setBackground(Color.WHITE);
		messageSplit.setRight(scroll);
		messageSplit.setDividerX(Math.max(messageSplit.getDividerX(),200));
	}

	public void showCaretList(ArrayList<TextCaret> caretList) {
		HashMap<String,ArrayList<TextCaret>> organizedCarets = new HashMap<String,ArrayList<TextCaret>>();
		for (TextCaret caret : caretList) {
			if (caret!=null && caret.getLineList()!=null && caret.getLineList().getParent()!=null && caret.getLineList().getParent().getFile()!=null) {
				CodeFile file = caret.getLineList().getParent().getFile();
				String name = file.getName().toString();
				ArrayList<TextCaret> fileCarets = organizedCarets.get(name);
				if (fileCarets==null) {
					fileCarets = new ArrayList<TextCaret>();
					organizedCarets.put(name,fileCarets);
				}
				fileCarets.add(caret);
			}
			else {
				ArrayList<TextCaret> nowhereCarets = organizedCarets.get("(not in a particular file)");
				if (nowhereCarets==null) {
					nowhereCarets = new ArrayList<TextCaret>();
					organizedCarets.put("(not in a particular file)",nowhereCarets);
				}
				nowhereCarets.add(caret);
			}
		}
		int numFiles = organizedCarets.keySet().size();
		int numRows = 1+numFiles+caretList.size();
		int[] levels = new int[numRows];
		String[] texts = new String[numRows];
		Caret[] carets = new Caret[numRows];
		ImageIcon[] icons = new ImageIcon[numRows];
		levels[0] = 0;
		texts[0] = caretList.size()+" occurrence"+(caretList.size()!=1 ? "s":"")+" in "+numFiles+" file"+(numFiles!=1 ? "s":"");
		carets[0] = null;
		icons[0] = Images.FOUND_ICON;
		int row = 1;
		for (String file : organizedCarets.keySet()) {
			levels[row] = 1;
			texts[row] = file;
			ArrayList<TextCaret> caretsInFile = organizedCarets.get(file);
			LineList list = (caretsInFile.get(0)!=null) ? caretsInFile.get(0).getLineList() : null;
			if (list!=null && list.getParent()!=null && list.getParent().getFile()!=null) {
				CodeFile codeFile = list.getParent().getFile();
				carets[row] = new TextCaret(codeFile.getCaretRef(),codeFile.getName(),0,0,0);
				icons[row] = codeFile.getIcon();
			}
			else {
				carets[row] = null;
				icons[row] = null;
			}
			++row;
			for (TextCaret caret : caretsInFile) {
				levels[row] = 2;
				texts[row] = (caret!=null) ? caret.getLineList().getParent()+"("+caret.getFirstLine()+","+caret.getFirstLineCol()+"): " : "(no location): ";
				texts[row] += (caret!=null) ? caret.getLineList().get(caret.getLine()).toString().replace('\t',' ') : "";
				carets[row] = caret;
				icons[row] = Images.FOUND_ICON;
				++row;
			}
		}
		ReferencePanel caretPanel = new ReferencePanel(this,levels,texts,carets,icons);
		CustomScrollPane scroll = new CustomScrollPane(caretPanel);
		caretPanel.setBackground(Color.WHITE);
		messageSplit.setRight(scroll);
		messageSplit.setDividerX(Math.max(messageSplit.getDividerX(),200));
	}

	public void runCompiledProgram(String filename,final Thread callback) {
		ReferencePanel errorPanel = new ReferencePanel(this,new int[]{0},new String[]{"Successfully compiled "+filename},new Caret[]{null},new ImageIcon[]{Images.SUCCESS_ICON});
		CustomScrollPane scroll = new CustomScrollPane(errorPanel);
		errorPanel.setBackground(Color.WHITE);
		messageSplit.setRight(scroll);
		messageSplit.setDividerX(24);
		justHadSuccessfulCompile = true;

		// Don't run the executable if the OS won't support running it or if it wasn't meant to be run (e.g. library)
		if (operatingSystem==OS_WINDOWS && filename.endsWith(".exe")) {
			File directory = project.getFile().getParentFile();
			System.out.println("Executing "+filename);
			try {
				// Start the program running
				runningProcess = Runtime.getRuntime().exec(filename,null,directory);
				// Switch the run button with the stop button
				Container parent = runButton.getParent();
				for (int i=0;i<parent.getComponentCount();++i) {
					if (parent.getComponent(i)==runButton) {
						runButton.mouseExited(new MouseEvent(runButton,0,new Date().getTime(),0,0,0,0,false));
						stopButton.setLocation(runButton.getLocation());
						parent.remove(i);
						parent.add(stopButton,i);
						parent.repaint();
						break;
					}
				}
				// Start a thread waiting for the program to end, to switch the button back
				Thread thread = new Thread() {
					public void run() {
						try {
							runningProcess.waitFor();
						}
						catch (InterruptedException e) {
							e.printStackTrace();
						}
						runningProcess = null;
						// Switch the run button with the stop button
						Container parent = stopButton.getParent();
						for (int i=0;i<parent.getComponentCount();++i) {
							if (parent.getComponent(i)==stopButton) {
								stopButton.mouseExited(new MouseEvent(stopButton,0,new Date().getTime(),0,0,0,0,false));
								runButton.setLocation(stopButton.getLocation());
								parent.remove(i);
								parent.add(runButton,i);
								parent.repaint();
								break;
							}
						}
						if (callback!=null) {
							callback.start();
						}
					}
				};
				thread.start();
			}
			catch (IOException e) {
				ErrorHandler.showException(e,"An error occurred when trying to start running "+filename+":");
			}
		}
	}

	public void showProjectOptions() {
		// Use a different UndoStack for inside the dialog box, since it uses temporary data before updating.
		UndoStack regularUndos = undos;
		undos = new UndoStack();
		ProjectOptionsDialog dialog = new ProjectOptionsDialog(this,project);
		dialog.setVisible(true);
		// TODO: Figure out a good way to make project changes undoable.  It may be confusing to the user if implemented
		undos = regularUndos;
	}

	/**
	 * Performs an SVN commit operation on the current project in a new thread, saving the project first.
	 */
	public void svnCommit() {
		if (project==null) {
			return;
		}
		// Save before committing
		saveAll();
		final TextAreaDialog messageDialog = new TextAreaDialog(editor,null,"Please enter a commit message to describe your changes:");
		messageDialog.setVisible(true);
		Thread thread = new Thread(){
			public void run() {
				// Wait until the user has pressed one of the buttons
				while (messageDialog.getSelected()==TextAreaDialog.NONE_SELECTED) {
					try {
						Thread.sleep(50);
					}
					catch (InterruptedException e1) {
						// This is the silliest exception ever
					}
				}
				if (messageDialog.getSelected()==TextAreaDialog.OK) {
					String commitMessage = messageDialog.getInputText();
					MessagePopup popup = new MessagePopup(editor,null,"Performing SVN Commit...");
					popup.setVisible(true);
					try {
						SVNCommitInfo info = SVNController.commit(project.getFile().getParentFile(),true,commitMessage);
						String displayText;
						if (info.getErrorMessage()==null) {
							displayText = "SVN Commit Complete!\nThe repository is now at revision "+info.getNewRevision()+".";
						}
						else {
							displayText = "Sorry, the SVN commit didn't work.\nThe following message was given:\n\n\""+info.getErrorMessage()+"\"";
						}
						if (popup.isVisible()) {
							popup.setText(displayText);
						}
						else {
							popup = new MessagePopup(editor,null,displayText);
							popup.setVisible(true);
						}
					}
					catch (SVNException e1) {
						popup.setVisible(false);
						ErrorHandler.showMessage("Sorry, the SVN commit didn't work.\nThe following messages were given:\n\n\""+e1.getMessage()+"\"\n\n\""+e1.getErrorMessage()+"\"");
					}
				}
			}
		};
		thread.start();
	}

	/**
	 * Performs an SVN update operation on the current project in a new thread, and reopens the project to refresh any changes.
	 */
	public void svnUpdate() {
		if (project==null) {
			return;
		}
		Thread thread = new Thread(){
			public void run() {
				MessagePopup popup = new MessagePopup(editor,null,"Performing SVN Update...");
				popup.setVisible(true);
				try {
					File projectFile = project.getFile();
					File projectDirectory = projectFile.getParentFile();
					// Close the project so that reopening the project won't save the files
					closeProject();
					long revision = SVNController.update(projectDirectory, SVNRevision.HEAD,true);
					String displayText = "SVN Update Complete!\nYour working copy is now at revision "+revision+".";
					if (popup.isVisible()) {
						popup.setText(displayText);
					}
					else {
						popup = new MessagePopup(editor,null,displayText);
						popup.setVisible(true);
					}
					// Reopen the project after the update
					openProject(projectFile);
				}
				catch (SVNException e1) {
					popup.setVisible(false);
					ErrorHandler.showMessage("Sorry, the SVN update didn't work.\nThe following messages were given:\n\n\""+e1.getMessage()+"\"\n\n\""+e1.getErrorMessage()+"\"");
				}
			}
		};
		thread.start();
	}

	/**
	 * Runs Natural Docs on the current project in a new thread
	 */
	public void runNaturalDocs() {
		if (project==null) {
			return;
		}
		Thread thread = new Thread(){
			public void run() {
				String projectPath = project.getPath()+ File.separator;
				String docsPath = projectPath+"Documentation";
				File docsDirectory = new File(docsPath);
				if (!docsDirectory.exists()) {
					docsDirectory.mkdir();
				}
				File natDocInfoDirectory = new File(docsPath+File.separator+"NatDocInfo");
				if (!natDocInfoDirectory.exists()) {
					natDocInfoDirectory.mkdir();
				}
				String menuFilePath = docsPath+File.separator+"NatDocInfo"+File.separator+"Menu.txt";
				File menuFile = new File(menuFilePath);
				if (!menuFile.exists()) {
					try {
						String menuString = FileUtil.readFileToString(new File("resources"+File.separator+"NaturalDocsMenu.txt"));
						PrintStream file = new PrintStream(menuFile);
						String projectFile = project.getFile().getName();
						String projectName = projectFile.substring(0,(projectFile.contains(".")) ? projectFile.lastIndexOf('.') : projectFile.length());
						file.print("Format: Development Release 02-10-2007 (1.35 base)\n\n\nTitle: "+projectName);
						file.print(menuString);
					}
					catch (IOException e1) {
						ErrorHandler.showMessage("An error occurred trying to copy resources"+File.separator+"NaturalDocsMenu.txt to set up Natural Docs for this project.");
					}
				}
				String styleFilePath = docsPath+File.separator+"NatDocInfo"+File.separator+"Style.css";
				File styleFile = new File(styleFilePath);
				if (!styleFile.exists()) {
					try {
						FileUtil.copyFile("resources"+File.separator+"NaturalDocsStyle.css",styleFilePath);
					}
					catch (IOException e1) {
						ErrorHandler.showMessage("An error occurred trying to copy resources"+File.separator+"NaturalDocsStyle.css to set up Natural Docs for this project.");
					}
				}
				File stylesDirectory = new File(docsPath+File.separator+"styles");
				if (!stylesDirectory.exists()) {
					stylesDirectory.mkdir();
					try {
						FileUtil.copyFile("resources"+File.separator+"NaturalDocsGradient.bmp",docsPath+File.separator+"styles"+File.separator+"gradient.bmp");
						FileUtil.copyFile("resources"+File.separator+"NaturalDocsSide.bmp",docsPath+File.separator+"styles"+File.separator+"side.bmp");
					}
					catch (IOException e1) {
						ErrorHandler.showMessage("An error occurred trying to copy resources"+File.separator+"NaturalDocsGradient.bmp and resources"+File.separator+"NaturalDocsSide.bmp to set up Natural Docs for this project.");
					}
				}

				try {
					String commandLine = "\""+settings.get("NaturalDocs")+"\" -i "+project.getPath()+" -o FramedHTML "+docsPath+" -p "+docsPath+File.separator+"NatDocInfo -img *"+File.separator+"Images -s Style -r -xi "+docsPath;
					Process process = Runtime.getRuntime().exec(commandLine);
					BufferedReader output = new BufferedReader(new InputStreamReader(process.getInputStream()));
					BufferedReader output2 = new BufferedReader(new InputStreamReader(process.getErrorStream()));
					String line;
					String previousLine = null;
					MessagePopup popup = new MessagePopup(editor,null,"Running Natural Docs...");
					popup.setVisible(true);
		//							System.out.println("Running Natural Docs with command line:\n"+commandLine+"\n");
					StringBuffer buf = new StringBuffer();
					while ((line = output.readLine())!=null || (line = output2.readLine())!=null) {
		//								System.out.println(line);
						buf.append(line).append('\n');
						previousLine = line;
						if (popup.isVisible()) {
							popup.setText("Running Natural Docs...\n\n"+line);
						}
					}
					int exitCode = process.waitFor();
		//							System.out.println("Done running Natural Docs");
					String displayText = "Done running Natural Docs";
					if (previousLine==null || !previousLine.startsWith("Done")) {
						displayText = "Sorry, an error occurred running Natural Docs.  This is its output:\n\n"+buf;
					}
					if (popup.isVisible()) {
						popup.setText(displayText);
					}
					else {
						popup = new MessagePopup(editor,null,displayText);
						popup.setVisible(true);
					}
				}
				catch (Exception e1) {
					ErrorHandler.showMessage("Sorry, running Natural Docs didn't work.\n\nMake sure that Natural Docs is at \""+settings.get("NaturalDocs")+"\" or change the directory in Inventor.ini");
				}
			}
		};
		thread.start();
	}

	/**
	 * Gets the settings from the configuration file.
	 * @return a map mapping attribute names to their values
	 */
	private static TreeMap<String,String> loadSettings() {
		try {
			BufferedReader input = new BufferedReader(new FileReader(CONFIG_FILE));
			TreeMap<String,String> settings = new TreeMap<String,String>();
			String line;
			while ((line=input.readLine())!=null) {
				line = line.trim();
				if (!line.startsWith(";") && line.contains("=")) {
					int equalsIndex = line.indexOf('=');
					settings.put(line.substring(0,equalsIndex).trim(),line.substring(equalsIndex+1).trim());
				}
			}
			return settings;
		} catch (IOException e) {
			return null;
		}
	}

	/**
	 * Saves the settings to the configuration file.
	 * @param settings a map mapping attribute names to their values
	 */
	private static void saveSettings(TreeMap<String,String> settings) {
		if (settings!=null) {
			try {
				PrintStream file = new PrintStream(CONFIG_FILE);
				file.println(";Inventor IDE Configuration File");
				for (Map.Entry<String,String> entry : settings.entrySet()) {
					file.println(entry.getKey()+" = "+entry.getValue());
				}
				file.close();
			}
			catch (FileNotFoundException e) {
				ErrorHandler.showMessage("Sorry, the settings file "+CONFIG_FILE+" couldn't be found for the settings to be saved.");
			}
		}
	}

	public Project getProject() {
		return project;
	}

	public void createNewProject(File file) {
		try {
			BufferedWriter output = new BufferedWriter(new FileWriter(file));
			String name = file.getName();
			output.write(Project.getNewProjectText(name.substring(0,name.length()-4)));
			output.close();
			openProject(file);
		}
		catch (IOException e1) {
			ErrorHandler.showException(e1);
		}
	}

	public ProjectFile createNewFile(File file) {
		// TODO: Support creating a new file in an empty default project
		if (project!=null) {
			String name = file.getName();
			int lastDot = name.lastIndexOf('.');
			if (lastDot>=0) {
				String extension = name.substring(lastDot+1).toLowerCase();
				if (extension.equals("asm")) {
					CodeFile codeFile = new CodeFile(name);
					codeFile.setProject(project);
					return codeFile;
				}
				else if (extension.equals("inc")) {
					CodeFile codeFile = new CodeFile(name,"",true);
					codeFile.setProject(project);
					return codeFile;
				}
			}
		}
		return null;
	}

	public void addFile() {
		addFile(project.getFile().getParent());
	}
	public void addFile(String initialDirectory) {
		if (project==null) {
			return;
		}
		JFileChooser chooser = new JFileChooser(initialDirectory);
		chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
		chooser.setMultiSelectionEnabled(false);
		chooser.setFileFilter(SUPPORTED_FILES_FILTER);
		if (chooser.showOpenDialog(PwnIDE.editor)==JFileChooser.APPROVE_OPTION) {
			File file = chooser.getSelectedFile();
			if (file.isDirectory()) {
				ErrorHandler.showNotImplementedMessage("The file \""+file.getPath()+"\" is a directory, and adding of entire directories isn't supported yet.");
			}
			CustomFileFilter filter = (CustomFileFilter)chooser.getFileFilter();
			if (!filter.accept(file) && filter.extensions.length>0) {
				file = new File(file.getPath()+"."+filter.extensions[0]);
			}
			// Get the path relative to the project root
			// Start by getting the project directory split up
			File projectDirectory;
			try {
				projectDirectory = project.getFile().getCanonicalFile().getParentFile();
			}
			catch (IOException e1) {
				ErrorHandler.showException(e1);
				return;
			}
			ArrayList<File> projectPath = new ArrayList<File>();
			projectPath.add(projectDirectory);
			File dir = projectDirectory;
			// NOTE: This hack with pathName is to avoid the bug in which Windows thinks that a folder "Computer" is the parent of a drive.
			String pathName = dir.getPath();
			while ((dir = dir.getParentFile())!=null && pathName.startsWith(dir.getPath())) {
				projectPath.add(0,dir);
				pathName = dir.getPath();
			}
			// Then get the file's directory split up
			ArrayList<File> filePath = new ArrayList<File>();
			dir = file;
			// NOTE: This is the same hack with pathName as above.
			pathName = dir.getPath();
			while ((dir = dir.getParentFile())!=null && pathName.startsWith(dir.getPath())) {
				filePath.add(0,dir);
				pathName = dir.getPath();
			}
//							System.out.println("Size of filePath: "+filePath.size());
//							for (File f : filePath) {
//								System.out.println(f);
//							}
//							System.out.println("Size of projectPath: "+projectPath.size());
//							for (File f : projectPath) {
//								System.out.println(f);
//							}
			if (filePath.isEmpty() || !filePath.get(0).equals(projectPath.get(0))) {
				ErrorHandler.showMessage("Sorry, there isn't support for adding files that are on a different drive from the project.\nYou'll have to either move/copy the file or move/copy the project.");
				return;
			}
			int i;
			ArrayList<String> relativePath = new ArrayList<String>();
			for (i=1;i<filePath.size() && i<projectPath.size();++i) {
				// Case of file's directory not being an ancestor or decendant of the project directory
				if (!filePath.get(i).equals(projectPath.get(i))) {
					for (int j=0;j<projectPath.size()-i;++j) {
						relativePath.add("..");
					}
					for (int j=i;j<filePath.size();++j) {
						relativePath.add(filePath.get(j).getName());
					}
				}
			}
			if (relativePath.isEmpty()) {
				// Case of file's directory being a decendant of the project directory
				if (filePath.size()>projectPath.size()) {
					for (int j=i;j<filePath.size();++j) {
						relativePath.add(filePath.get(j).getName());
					}
				}
				// Case of file's directory being an ancestor of the project directory
				else if (filePath.size()<projectPath.size()) {
					for (int j=i;j<projectPath.size();++j) {
						relativePath.add("..");
					}
				}
				// The case of file's directory being equal the project directory needs no handling
			}
			// Now have the path of the file relative to the path of the project

			Triple<Directory,DefaultMutableTreeNode,ArrayList<TreeNode>> directoryAndNode = projectPanel.addFile(relativePath,file);
			final Directory modelDirectory = directoryAndNode.a;
			final DefaultMutableTreeNode treeNode = directoryAndNode.b;
			final ArrayList<TreeNode> treePath = directoryAndNode.c;
			if (file.exists()) {
				// Make sure that the file isn't already in the project
				if (modelDirectory.getCodeFiles().containsKey(file.getName())) {
					ErrorHandler.showMessage("The file \""+file+"\" is already in this project, so you don't need to add it.");
					return;
				}
				final File finalFile = file;
				Thread thread = new Thread() {
					public void run() {
						JWindow loadingScreen = new JWindow(PwnIDE.this){
							public void paint(Graphics g) {
								super.paint(g);
								g.setColor(Color.WHITE);
								g.fillRect(0,0,getWidth(),getHeight());
								g.setColor(CustomDialog.LINE_COLOUR);
								g.drawRect(0,0,getWidth()-1,getHeight()-1);
								final int offset = 20;
								g.setColor(CustomDialog.FONT_COLOUR);
								g.setFont(CustomDialog.FONT);
								g.drawString("Loading File "+finalFile+"...",offset,offset+CustomDialog.FONT.getSize()-4);
							}
						};
						try {
							String name = finalFile.getName();
							int extensionStart = name.lastIndexOf('.')+1;
							CodeFile codeFile = null;
							if (extensionStart>0) {
								String extension = name.substring(extensionStart).toLowerCase();
								Language language = Loader.EXTENSION_TO_LANGUAGE_MAP.get(extension);
								if (language!=null) {
									GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
									if (screens!=null && screens.length>0) {
										DisplayMode mode = screens[0].getDisplayMode();
										loadingScreen.setBounds((mode.getWidth()-664)>>>1,(mode.getHeight()-48)>>>1,664,48+32);
									}
									loadingScreen.setLayout(null);
									progress = new CustomProgressBar(624,0.01f);
									progress.setLocation((664-624)>>1,48-4);
									loadingScreen.add(progress);
									loadingScreen.setVisible(true);
									progress.repaint();
									codeFile = new CodeFile(name,language,modelDirectory,false,language.getDefaultIncludeExtension().equals(extension));
									// NOTE: Must add file to the directory and set its project before loading so that the loader can find the file.
									modelDirectory.add(codeFile);
									codeFile.setProject(project);
									Loader.loadFile(project,codeFile,progress);
									projectPanel.addNode(treeNode,codeFile,treePath);
									loadingScreen.setVisible(false);
								}
							}
							if (codeFile==null) {
								loadingScreen.setVisible(false);
								ErrorHandler.showMessage("Sorry, I can't open the file \""+finalFile+"\".");
							}
						}
						catch (Throwable e) {
							loadingScreen.setVisible(false);
							ErrorHandler.showException(e);
						}
					}
				};
				thread.start();
			}
			else {
				ProjectFile fileObject = editor.createNewFile(file);
				if (fileObject!=null) {
					if (fileObject instanceof CodeFile) {
						modelDirectory.add((CodeFile)fileObject);
					}
					projectPanel.addNode(treeNode,fileObject,treePath);
				}
			}
			editor.saveAll();
		}
	}

	public void openProject(final File file) {
		if (file!=null) {
			if (project!=null) {
				closeProject();
				System.gc();
			}
			Thread thread = new Thread() {
				public void run() {
					JWindow loadingScreen = new JWindow(PwnIDE.this) {
						public void paint(Graphics g) {
							super.paint(g);
							g.setColor(Color.WHITE);
							g.fillRect(0,0,getWidth(),getHeight());
							g.setColor(CustomDialog.LINE_COLOUR);
							g.drawRect(0,0,getWidth()-1,getHeight()-1);
							final int offset = 20;
							g.setColor(CustomDialog.FONT_COLOUR);
							g.setFont(CustomDialog.FONT);
							g.drawString("Loading Project "+file+"...",offset,offset+CustomDialog.FONT.getSize()-4);
						}
					};
					try {
						GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
						if (screens!=null && screens.length>0) {
							DisplayMode mode = screens[0].getDisplayMode();
							loadingScreen.setBounds((mode.getWidth()-664)>>>1,(mode.getHeight()-48)>>>1,664,48+32);
						}
						loadingScreen.setLayout(null);
						progress = new CustomProgressBar(624,0.01f);
						progress.setLocation((664-624)>>1,48-4);
						loadingScreen.add(progress);
						loadingScreen.setVisible(true);
						progress.repaint();
						project = new Project(file,progress);
						projectPanel.setProject(project);
						ui.showOpenFiles();
						settings.put("MostRecentProject",file.getCanonicalPath());
						loadingScreen.setVisible(false);
					}
					catch (IOException e) {
						loadingScreen.setVisible(false);
						ErrorHandler.showMessage("Sorry, an error occurred trying to open the project \""+file+"\".");
					}
				}
			};
			thread.start();
		}
	}

	private void closeProject() {
		if (project!=null) {
			saveAll();
			ui.closeAllTabs();
			project = null;
			projectPanel.setProject(null);
			messageSplit.setRight(new JPanel());
			messageSplit.setDividerX(Math.max(messageSplit.getDividerX(),0));
			undos.clear();
		}
	}

	public void saveAll() {
		try {
			project.save();
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to save your project.\nIt may or may not have been saved.");
		}
	}
	public void undo() {
		try {
			hideSuccessfulCompile();
			undos.undo();
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to undo.\nIt may or may not have been undone, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}
	public void redo() {
		try {
			hideSuccessfulCompile();
			undos.redo();
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to redo.\nIt may or may not have been redone, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}
	public void eventDone(Undoable event) {
		hideSuccessfulCompile();
		undos.add(event);
	}

	public void mergeWithUndoTop(Undoable event) {
		hideSuccessfulCompile();
		undos.mergeWithUndoTop(event);
	}

	public void hideSuccessfulCompile() {
		if (justHadSuccessfulCompile) {
			justHadSuccessfulCompile = false;
			messageSplit.setHidden(true);
		}
	}
	
	public Undoable getUndoTop() {
		return undos.getUndoTop();
	}

	public ProjectPanel getProjectPanel() {
		return projectPanel;
	}

	public void setModalDialog(CustomDialog dialog) {
		modalDialog = dialog;
	}


	public void windowGainedFocus(WindowEvent e) {
		ui.updateFocus();
	}

	public void windowLostFocus(WindowEvent e) {

	}

	/**
	 * Removes the {@link CodeFileChunk}s in the {@link HeaderCaret} and their {@link Header}s
	 * @param caret the selection of {@link CodeFileChunk}s
	 * @param file the {@link CodeFile} containing the {@link CodeFileChunk}s
	 */
	public void removeSelectedHeaders(HeaderCaret caret,CodeFile file) {
		ArrayList<CodeFileChunk> selection = caret.getSelection();
		ArrayList<Undoable> events = new ArrayList<Undoable>();
		// First, remove all CodeFileChunks that are not Sections (in any order)
		for (Iterator<CodeFileChunk> it = selection.iterator();it.hasNext();) {
			CodeFileChunk chunk = it.next();
			if (!(chunk instanceof Section)) {
				ObservedList<CodeFileChunk> list = chunk.getParent().getContent();
				int index = list.indexOf(chunk);
				list.remove(index);
				events.add(new ListRemoveEvent<CodeFileChunk>(list,chunk,index));
				it.remove();
			}
		}
		// Now only Sections are left, so remove all Sections (in any order)
		for (Iterator<CodeFileChunk> it = selection.iterator();it.hasNext();) {
			Section section = (Section)it.next();
			ArrayList<Undoable> sectionRemovalEvents = new ArrayList<Undoable>();
			ObservedList<CodeFileChunk> list = section.getParent().getContent();
			int index = list.indexOf(section);
			// Move any content of the Section so that it doesn't disappear
			if (!section.getContent().isEmpty()) {
				// If this Section is preceded by another Section, move the content there
				if (index>0 && (list.get(index-1) instanceof Section)) {
					ObservedList<CodeFileChunk> sourceList = section.getContent();
					ObservedList<CodeFileChunk> destList = ((Section)list.get(index-1)).getContent();
					for (int i=sourceList.size()-1;i>=0;--i) {
						CodeFileChunk innerChunk = sourceList.remove(i);
						sectionRemovalEvents.add(new ListRemoveEvent<CodeFileChunk>(sourceList,innerChunk,i));
						destList.add(innerChunk);
						sectionRemovalEvents.add(new ListAddEvent<CodeFileChunk>(destList,destList.size()-1));
					}
				}
				// If this Section is not precedded by another Section, move the content to the parent
				else {
					ObservedList<CodeFileChunk> sourceList = section.getContent();
					int numToMove = sourceList.size();
					for (int i=numToMove-1;i>=0;--i) {
						CodeFileChunk innerChunk = sourceList.remove(i);
						sectionRemovalEvents.add(new ListRemoveEvent<CodeFileChunk>(sourceList,innerChunk,i));
						list.add(index,innerChunk);
						sectionRemovalEvents.add(new ListAddEvent<CodeFileChunk>(list,index));
					}
					index += numToMove;
				}
			}
			list.remove(index);
			sectionRemovalEvents.add(new ListRemoveEvent<CodeFileChunk>(list,section,index));
			events.add(new UndoSet(sectionRemovalEvents,"Removing "+section+" from "+file));
			it.remove();
		}

		if (events.size()==1) {
			eventDone(events.get(0));
		}
		else if (events.size() > 1) {
			eventDone(new UndoSet(events,"Removing "+events.size()+" chunks of code from "+file.getName()));
		}
	}

	/**
	 * Copies the {@link CodeFileChunk}s in the {@link HeaderCaret}
	 * @param caret the selection of {@link CodeFileChunk}s
	 */
	public void copySelectedHeaders(HeaderCaret caret) {
		try {
			ArrayList<CodeFileChunk> selection = caret.getSelection();
			ObservedList<CodeFileChunk> list = new ObservedList<CodeFileChunk>(selection.size());
			for (CodeFileChunk chunk : selection) {
				list.add(chunk);
			}
			ClipboardUtil.setClipboardJavaObject(list);
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to copy.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	/**
	 * Cuts the {@link CodeFileChunk}s in the {@link HeaderCaret}
	 * @param caret the selection of {@link CodeFileChunk}s
	 * @param file the file from which the {@link CodeFileChunk}s are cut
	 */
	public void cutSelectedHeaders(HeaderCaret caret,CodeFile file) {
		try {
			ArrayList<CodeFileChunk> selection = caret.getSelection();
			ObservedList<CodeFileChunk> list = new ObservedList<CodeFileChunk>(selection.size());
			for (CodeFileChunk chunk : selection) {
				list.add(chunk);
			}
			ClipboardUtil.setClipboardJavaObject(list);
			removeSelectedHeaders(caret,file);
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to cut.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	/**
	 * Shifts down the {@link CodeFileChunk}s in the {@link HeaderCaret}
	 * @param caret the selection of {@link CodeFileChunk}s
	 * @param file the file in which the {@link CodeFileChunk}s are shifted
	 */
	public void shiftDownSelectedHeaders(HeaderCaret caret,CodeFile file) {
		try{
			ArrayList<Undoable> events = new ArrayList<Undoable>();
			// Don't want a deep copy, and only want CodeFileChunks
			ArrayList<CodeFileChunk> originalSelection = caret.getSelection();
			ArrayList<CodeFileChunk> selection = new ArrayList<CodeFileChunk>(originalSelection.size());
			for (CodeFileChunk chunk : originalSelection) {
				selection.add(chunk);
			}
			while (!selection.isEmpty()) {
				ArrayList<CodeFileChunk> block = extractHeaderBlock(selection);
				CodeFileChunk nextChunk = getNextHeader(block.get(block.size()-1));
				// If this block is not at the end of the file,
				if (nextChunk!=null) {
					// Case 1: nextChunk is not a Section
					if (!(nextChunk instanceof Section)) {
						events.add(nextChunk.getParent().remove(nextChunk));
						Section parent = block.get(0).getParent();
						int index = parent.getContent().indexOf(block.get(0));
						if (index==0 || !(parent.getContent().get(index-1) instanceof Section)) {
							events.add(parent.add(nextChunk,index));
						}
						else {
							parent = (Section)parent.getContent().get(index-1);
							events.add(parent.add(nextChunk));
						}
					}
					else {
						Section section = (Section)nextChunk;
						Section lastSection = null;
						for (CodeFileChunk chunk : block) {
							if (chunk instanceof Section) {
								lastSection = (Section)chunk;
							}
						}
						// Case 2: nextChunk is a Section and block contains no Sections
						if (lastSection==null) {
							for (int i=block.size()-1;i>=0;--i) {
								CodeFileChunk chunk = block.get(i);
								events.add(chunk.getParent().remove(chunk));
								events.add(section.add(chunk,0));
							}
						}
						// Case 3: nextChunk is a Section and block contains at least one Section
						else {
							// Move the content of nextChunk to the end of the last Section in block
							while (!section.getContent().isEmpty()) {
								CodeFileChunk chunk = section.getContent().get(0);
								events.add(section.remove(chunk));
								events.add(lastSection.add(chunk));
							}
							// Find the parent into which nextChunk will be moved
							Section parent = block.get(0).getParent();
							int index = parent.getContent().indexOf(block.get(0));
							if (index!=0 && (parent.getContent().get(index-1) instanceof Section)) {
								parent = (Section)parent.getContent().get(index-1);
								index = parent.getParent().getContent().indexOf(parent)+1;
								parent = parent.getParent();
							}
							// Move the content of block that is before any Section, into nextChunk at the end
							for (CodeFileChunk chunk : block) {
								if (!(chunk instanceof Section)) {
									events.add(chunk.getParent().remove(chunk));
									events.add(section.add(chunk));
								}
								else {
									// Move nextChunk to before the first section in the block
									events.add(section.getParent().remove(section));
									events.add(parent.add(section, index));
									break;
								}
							}
						}
					}
				}
			}
			if (events.size()==1) {
				eventDone(events.get(0));
			}
			else if (events.size() > 1) {
				eventDone(new UndoSet(events,"Shifting down "+events.size()+" chunks of code in "+file.getName()));
			}
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to shift down.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	/**
	 * Shifts up the {@link CodeFileChunk}s in the {@link HeaderCaret}
	 * @param caret the selection of {@link CodeFileChunk}s
	 * @param file the file in which the {@link CodeFileChunk}s are shifted
	 */
	public void shiftUpSelectedHeaders(HeaderCaret caret,CodeFile file) {
		try {
			ArrayList<Undoable> events = new ArrayList<Undoable>();
			// Don't want a deep copy, and only want CodeFileChunks
			ArrayList<CodeFileChunk> originalSelection = caret.getSelection();
			ArrayList<CodeFileChunk> selection = new ArrayList<CodeFileChunk>(originalSelection.size());
			for (CodeFileChunk chunk : originalSelection) {
				selection.add(chunk);
			}
			while (!selection.isEmpty()) {
				ArrayList<CodeFileChunk> block = extractHeaderBlock(selection);
				CodeFileChunk prevChunk = getPrevHeader(block.get(0));
				// If this block is not at the beginning of the file,
				if (prevChunk!=null && !(prevChunk instanceof CodeFile)) {
					// Case 1: prevChunk is not a Section
					if (!(prevChunk instanceof Section)) {
						events.add(prevChunk.getParent().remove(prevChunk));
						CodeFileChunk lastChunk = block.get(block.size()-1);
						Section parent = (lastChunk instanceof Section) ? (Section)lastChunk : lastChunk.getParent();
						int index = (lastChunk==parent) ? 0 : (parent.getContent().indexOf(lastChunk)+1);
						events.add(parent.add(prevChunk,index));
					}
					else {
						Section section = (Section)prevChunk;
						Section lastSection = null;
						for (CodeFileChunk chunk : block) {
							if (chunk instanceof Section) {
								lastSection = (Section)chunk;
							}
						}
						// Case 2: prevChunk is a Section and block contains no Sections (so all of block is inside prevChunk)
						// (Case 3 requires the steps for Case 2 as well.)
						CodeFileChunk prevPrevChunk = getPrevHeader(section);
						Section newSection = (prevPrevChunk instanceof Section) ? (Section)prevPrevChunk : prevPrevChunk.getParent();
						int index = (newSection.getContent().contains(section)) ? newSection.getContent().indexOf(section) : newSection.getContent().size();
						for (CodeFileChunk chunk : block) {
							if (!(chunk instanceof Section)) {
								events.add(section.remove(chunk));
								events.add(newSection.add(chunk,index));
								++index;
							}
							else {
								break;
							}
						}
						if (lastSection==null) {
							// Finish up by putting prevChunk after the end of block
							events.add(prevChunk.getParent().remove(prevChunk));
							CodeFileChunk lastChunk = block.get(block.size()-1);
							Section parent = newSection;
							index = parent.getContent().indexOf(lastChunk)+1;
							if (!(parent instanceof CodeFile)) {
								index = parent.getParent().getContent().indexOf(parent)+1;
								parent = parent.getParent();
							}
							events.add(parent.add(prevChunk,index));
						}
						// Case 3: prevChunk is a Section and block contains at least one Section
						else {
							// Move the content of the last Section after block into prevChunk
							CodeFileChunk lastChunk = block.get(block.size()-1);
							CodeFileChunk next = getNextHeader(lastChunk);
							while (next!=null && !(next instanceof Section)) {
								events.add(lastSection.remove(next));
								events.add(section.add(next));
								next = getNextHeader(next);
							}
							// Finish up by putting prevChunk after the end of block
							events.add(prevChunk.getParent().remove(prevChunk));
							Section parent = lastChunk.getParent();
							index = parent.getContent().indexOf(lastChunk)+1;
							if (!(lastChunk instanceof Section)) {
								index = parent.getParent().getContent().indexOf(parent)+1;
								parent = parent.getParent();
							}
							events.add(parent.add(prevChunk,index));
						}
					}
				}
			}
			if (events.size()==1) {
				eventDone(events.get(0));
			}
			else if (events.size() > 1) {
				eventDone(new UndoSet(events,"Shifting down "+events.size()+" chunks of code in "+file.getName()));
			}
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to shift up.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	/**
	 * Finds the {@link CodeFileChunk} linearly following the specified one
	 * @param current the {@link CodeFileChunk} to start from
	 * @return the following {@link CodeFileChunk}, or null if <code>current</code> is at the end of the file
	 */
	public CodeFileChunk getNextHeader(CodeFileChunk current) {
		// If current is a non-empty Section, the next header is the first in the Section
		if ((current instanceof Section) && !((Section)current).getContent().isEmpty()) {
			return ((Section)current).getContent().get(0);
		}
		// Else, check for any following headers, up the tree if needed
		ObservedList<CodeFileChunk> content = current.getParent().getContent();
		int index = content.indexOf(current);
		if (index < (content.size()-1)) {
			return content.get(index+1);
		}
		current = current.getParent();
		if (!(current instanceof CodeFile)) {
			content = current.getParent().getContent();
			index = content.indexOf(current);
			if (index < (content.size()-1)) {
				return content.get(index+1);
			}
		}
		// No following header found
		return null;
	}

	/**
	 * Finds the {@link CodeFileChunk} linearly preceding the specified one
	 * @param current the {@link CodeFileChunk} to start from
	 * @return the previous {@link CodeFileChunk}, including the {@link CodeFile} if <code>current</code> is at the beginning of the file
	 */
	public CodeFileChunk getPrevHeader(CodeFileChunk current) {
		if (current instanceof CodeFile) {
			// There is nothing previous to the CodeFile
			return null;
		}
		ObservedList<CodeFileChunk> content = current.getParent().getContent();
		int index = content.indexOf(current);
		if (index==0) {
			return current.getParent();
		}
		CodeFileChunk previous = content.get(index-1);
		if ((previous instanceof Section) && !((Section)previous).getContent().isEmpty()) {
			content = ((Section)previous).getContent();
			previous = content.getLast();
		}
		return previous;
	}

	/**
	 * Extracts a maximal block of linearly contiguous {@link CodeFileChunk}s from the list (removing it from the list)
	 * @param selection the list of chunks to check for blocks
	 * @return a maximal block of linearly contiguous {@link CodeFileChunk}s in order
	 */
	private ArrayList<CodeFileChunk> extractHeaderBlock(ArrayList<CodeFileChunk> selection) {
		ArrayList<CodeFileChunk> block = new ArrayList<CodeFileChunk>();
		block.add(selection.remove(0));
		CodeFileChunk preHead = getPrevHeader(block.get(0));
		CodeFileChunk postTail = getNextHeader(block.get(0));
		boolean somethingAdded;
		do {
			somethingAdded = false;
			for (int i=0;i<selection.size();++i) {
				CodeFileChunk chunk = selection.get(i);
				if (chunk==postTail) {
					block.add(chunk);
					somethingAdded = true;
					postTail = getNextHeader(chunk);
					selection.remove(i);
					--i;
				}
				else if (chunk==preHead) {
					block.add(0,chunk);
					somethingAdded = true;
					preHead = getPrevHeader(chunk);
					selection.remove(i);
					--i;
				}
			}
		} while (somethingAdded);
		return block;
	}

	/**
	 * Pastes the {@link CodeFileChunk}s in the specified list after the current chunk
	 * @param current the chunk to precede the pasted chunks
	 * @param file the file in which to paste the chunks
	 * @param list the list of chunks to be pasted
	 */
	public void pasteCopiedHeaders(CodeFileChunk current,CodeFile file,ObservedList<CodeFileChunk> list) {
		try {
			ArrayList<Undoable> events = new ArrayList<Undoable>();
			if (current!=null && !(current instanceof CodeFile)) {
				Section parent = current.getParent();
				final Section originalParent = parent;
				int index = parent.getContent().indexOf(current);
				boolean followedByNonSection = (index+1<parent.getContent().size()) && !(parent.getContent().get(index+1) instanceof Section);
				boolean addedSectionToOriginal = false;
				for (CodeFileChunk chunk : list) {
					if (chunk instanceof Section && !(parent instanceof CodeFile)) {
						events.add(parent.getParent().addAfter(chunk,parent));
						parent = (Section)chunk;
						current = null;
					}
					else {
						events.add(parent.addAfter(chunk,current));
						if (chunk instanceof Section) {
							if (parent==originalParent) {
								addedSectionToOriginal = true;
							}
							parent = (Section)chunk;
							current = null;
						}
						else {
							current = chunk;
						}
					}
				}
				if (followedByNonSection && addedSectionToOriginal) {
					// In this case, parent is currently the Section last added to originalParent.
					// non-Sections following this Sections must be added to the Section
					int start=originalParent.getContent().indexOf(parent);
					for (++start;start<originalParent.getContent().size();) {
						CodeFileChunk chunk = originalParent.getContent().get(start);
						if (chunk instanceof Section) {
							break;
						}
						events.add(originalParent.remove(start));
						events.add(parent.add(chunk));
					}
				}
			}
			else {
				Section parent = file;
				for (CodeFileChunk chunk : list) {
					if (chunk instanceof Section && !(parent instanceof CodeFile)) {
						events.add(parent.getParent().addAfter(chunk,parent));
						parent = (Section)chunk;
					}
					else {
						events.add(parent.add(chunk));
						if (chunk instanceof Section) {
							parent = (Section)chunk;
						}
					}
				}
			}
			// Update Constants and Structures now that they're back in global scope
			for (CodeFileChunk chunk : list) {
				if (chunk instanceof Constants) {
					((Constants)chunk).nowHaveGlobalScope();
				}
				else if (chunk instanceof Structure) {
					((Structure)chunk).nowHaveGlobalScope();
				}
			}
			// Update Functions and GlobalVariables now that they're back in global scope and the constants and structures are there
			for (CodeFileChunk chunk : list) {
				if (chunk instanceof Function) {
					((Function)chunk).nowHaveGlobalScope();
				}
				else if (chunk instanceof GlobalVariable) {
					((GlobalVariable)chunk).nowHaveGlobalScope();
				}
			}
			eventDone(new UndoSet(events,"Pasting "+list.size()+" chunks of code in "+ui.getCurrentCodeFile()));
		}
		catch (Throwable t) {
			ErrorHandler.showException(t,"Sorry, an error occurred while trying to paste.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
		}
	}

	/**
	 * @param path a file path that may contain incorrect slash directions for the current operating system
	 * @return a path modified to be valid for the current operating system in terms of "\" versus "/"
	 */
	public static String getValidFilePath(String path) {
		String separator = (File.separator.equals("\\")) ? "\\\\" : File.separator;
		return path.replaceAll("[\\\\/]",separator);
	}

	public boolean handleCommonShortcuts(KeyEvent e) {
		CodeFile file;
		if (Configuration.SAVE_ALL.isTriggered(e)) {
			saveAll();
		}
		else if (modalDialog!=null && modalDialog.interceptsKeyEvents()) {
			if (e.getID()==KeyEvent.KEY_PRESSED) {
				modalDialog.keyPressed(e);
			}
			else if (e.getID()==KeyEvent.KEY_TYPED) {
				modalDialog.keyTyped(e);
			}
			else if (e.getID()==KeyEvent.KEY_RELEASED) {
				modalDialog.keyReleased(e);
			}
		}
		else if (Configuration.UNDO.isTriggered(e)) {
			undo();
		}
		else if (Configuration.REDO.isTriggered(e)) {
			redo();
		}
		else if ((file = ui.getCurrentCodeFile())!=null) {
			if (Configuration.PASTE.isTriggered(e)) {
				Object obj = ClipboardUtil.getClipboardJavaObject();
				if (obj!=null && (obj instanceof ObservedList)) {
					try {
						pasteCopiedHeaders(ui.getCurrentChunk(),file,(ObservedList<CodeFileChunk>)obj);
					}
					catch (Throwable t) {
						ErrorHandler.showException(t,"Sorry, an error occurred while trying to paste.\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
					}
				}
				if (obj==null) {
					return false;
				}
			}
			else {
				Caret caret = file.getCaretRef().get();
				if (caret==null) {
					return false;	// Let the components deal with it if nothing selected
				}
				if (caret instanceof TextCaret) {
					return false;	// Let LineFields handle their own TextCarets
				}
				if (caret instanceof HeaderCaret) {
					String actionText = "do something";
					try {
						if (Configuration.REMOVE.isTriggered(e) || Configuration.DELETE_FORWARD.isTriggered(e)) {
							removeSelectedHeaders((HeaderCaret)caret,file);
							actionText = "delete";
						}
						else if (Configuration.COPY.isTriggered(e)) {
							copySelectedHeaders((HeaderCaret)caret);
							actionText = "copy";
						}
						else if (Configuration.CUT.isTriggered(e)) {
							cutSelectedHeaders((HeaderCaret)caret,file);
							actionText = "cut";
						}
						else if (Configuration.SHIFT_DOWN.isTriggered(e)) {
							shiftDownSelectedHeaders((HeaderCaret)caret,file);
							actionText = "shift down";
						}
						else if (Configuration.SHIFT_UP.isTriggered(e)) {
							shiftUpSelectedHeaders((HeaderCaret)caret,file);
							actionText = "shift up";
						}
					}
					catch (Throwable t) {
						ErrorHandler.showException(t,"Sorry, an error occurred while trying to "+actionText+".\nIt may or may not have worked, and Inventor IDE might not be stable anymore,\nso I'd recommend backing up the files in your project, saving, then restarting Inventor IDE.\nIf the files don't work, the backed up files should still work.");
					}
				}
			}
		}
		else {
			return false;
		}
		return true;
	}

	public static void main(String[] args) {
//		System.out.println("Classpath: "+System.getProperty("java.class.path"));
//		System.out.println("Working Directory: "+System.getProperty("user.dir"));
		String classpath = System.getProperty("java.class.path");
		int jarIndex = classpath.indexOf("Inventor.jar");
		String directory = System.getProperty("user.dir")+File.separator;
		if (jarIndex>=0) {
			directory = classpath.substring(0,jarIndex);
		}
		// NOTE: These directories MUST be set before they are used, otherwise weird things happen, especially due to all of the images in Images being null.
		PWNIDE_DIR = directory;
		IMAGE_DIR = PWNIDE_DIR+"images"+File.separator;
		String projectName = null;
		if (args!=null && args.length!=0) {
			projectName = args[0];
			for (int i=1;i<args.length;++i) {
				projectName += " "+args[i];
			}
			if (projectName.startsWith("\"")) {
				projectName = projectName.substring(1);
			}
			if (projectName.endsWith("\"")) {
				projectName = projectName.substring(0,projectName.length()-1);
			}
		}
		// Identify the operating system
		String osName = System.getProperty("os.name").toLowerCase();
		if (osName.contains("win")) {
			operatingSystem = OS_WINDOWS;
		}
		else if (osName.contains("mac")) {
			operatingSystem = OS_MAC;
		}
		else if (osName.length()>=3 && osName.charAt(osName.length()-2)=='n' && osName.charAt(osName.length())=='x') {
			// The check for linux/unix etc. is whether it ends in n?x;
			operatingSystem = OS_UNIX;
		}
		else {
			operatingSystem = OS_UNKNOWN;
		}
		JWindow loadingScreen = new JWindow(){
			public void paint(Graphics g) {
				super.paint(g);
				g.setColor(Color.WHITE);
				g.fillRect(0,0,getWidth(),getHeight());
				g.setColor(CustomDialog.LINE_COLOUR);
				g.drawRect(0,0,getWidth()-1,getHeight()-1);
				final int offset = 20;
				final int spacing = 8;
				g.drawImage(Images.PWNIDE_LOGO,offset,offset,null);
				g.setColor(CustomDialog.FONT_COLOUR);
				g.setFont(CustomDialog.FONT);
				g.drawString("Loading Inventor IDE "+VERSION_STAGE_NAME+" "+VERSION_RELEASE_NUMBER+VERSION_BUILD_CHAR+"...",offset,offset+spacing+Images.PWNIDE_LOGO.getHeight(null)+CustomDialog.FONT.getSize());
				g.drawString("Developed as part of Code Cortex, where the software enters your brain.",offset,offset+spacing*2+Images.PWNIDE_LOGO.getHeight(null)+CustomDialog.FONT.getSize()*2);
			}
		};
		// Set the look and feel to be the system default, because Java's stuff looks horrible.
		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		}
		catch (Exception e) {}
		GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
		if (screens!=null && screens.length>0) {
			DisplayMode mode = screens[0].getDisplayMode();
			loadingScreen.setBounds((mode.getWidth()-664)>>>1,(mode.getHeight()-178)>>>1,664,178+32);
		}
		loadingScreen.setLayout(null);
		progress = new CustomProgressBar(624,0.06f);
		progress.setLocation((664-624)>>1,178-4);
		loadingScreen.add(progress);
		loadingScreen.setVisible(true);
		progress.repaint();
		new PwnIDE(projectName);
		editor.setVisible(true);
		loadingScreen.setVisible(false);
	}
}
